The OLE Controls Framework Sample Code for Authoring non-MFC Controls ===================================================================== This document is a short discussion on using the OLE Controls Framework to author new OLE controls. 0. Contents =---------= 1.0 Introduction 1.1 Target Audience 1.2 Structure of the Framework 1.3 Target Environment 2.0 Creating an OLE Control 2.1 Using the Control Wizard 2.2 Building the Control 2.3 Working with the In-Process server 3.0 Working with your OLE Control 3.1 Structure of a Control 3.2 Painting a Control 3.3 Handling Messages in a Control 3.4 Adding a Propert 3.5 Adding a Method 3.6 Adding an Event 3.7 Using Standard OLE Types 3.8 Throwing an Exception 4.0 Persistence 4.1 Text Persistence 4.2 Binary Persistence 5.0 Property Pages 5.1 Working with a Property Page 5.2 Navigating through Associated Objects 5.3 Marking your Page as Dirty. 6.0 String Manipulation 6.1 Types of Strings 6.2 Working with Strings 7.0 Localization 7.1 Setting up for Localization 8.0 Creating an Internet Aware Control 9.0 Miscellaneous 9.1 Recommended Reading 9.2 Host Specific Notes 9.2.1 Microsoft Access 95 1.0 Introduction =--------------= The OLE Controls Framework is a sample code base from which one can author new OLE controls for use in existing containers, such as Microsoft Visual Basic, Microsoft Access, and Microsoft FoxPro, or in future containers of OLE Controls. It differs from the Microsoft Visual C++ CDK in many ways. Most notably, this framework is intended to be considerably more 'bare bones'. Only minimal functionality is provided to the programmer. Little in the way of default handlers for various windows messages, OLE events et al has been provided. The ability to add them all is there, but the code is not. The code base has also been architected primarily for performance and reduced code size as much as possible. Whenever a choice between ease of use and performance arose, the latter was typically chosen. The code base is, however, extremely extensibile, and, of course, all the source code is there -- if something doesn't do what you want it to, make it. 1.1 Target Audience This framework targets a slightly more advanced programmer than the Microsoft Visual C++ Control Develoers Kit. Specifically, the programmer will be required to under- stand some of the fundamentals of OLE automation and dual interfaces. The user will have to be able to understand and modify an .ODL file on their own. In addition, the user will be required to understand and be able to work with OLE persistence interfaces, most notably IStream and IPersistStream. However, if it is not desired, the user will not be requried to have much knowledge of OLE embedding interfaces. Programmers who do not have specific performance requirements, those not familiar with many of the pertinent OLE technologies, or those who work primarily with the Microsoft Foundation Classes will find the MFC/CDK far more suited to their needs. 1.2 Structure of the Framework The directory structure of the OLE Controls Framework is as follows: \Controls + |- \AutoSample + | |- \Debug | |- \Release |- \Button + | |- \Debug | |- \Release |- \Circle + | |- \Debug | |- \Release |- \FontColor + | |- \Debug | |- \Release |- \Framework + | |- \Debug | |- \Release |- \Include |- \Invisible + | |- \Debug | |- \Release |- \Localize + | |- \Debug | |- \Release | |- \French |- \Template |- \Wizards The \Framework and \Include directories contain the core code for writing an OLE control. The Include directory contains the headers that most controls will get their information from, and the Framework directory contains the core functionality [in the COleControl class] which compiles in to a library [.LIB] form. In addition, the Wizards and Template directories contain the necessary files to generate a control. In the Wizards directory is a Microsoft Visual Basic 4.0 Project, CtlWiz.Vbp. Running this project under VB4 will generate a skeleton control for you which will compile right away. [Note: this wizard pretty much assumes you're going to place the control under the \Controls directory -- most of the paths generated are relative and will look for the \Framework directory in ..\Framework] A few samples are provided with the Framework. The Circle sample demonstrates the most trivial of OLE controls -- it has no interesting properties, no property pages, and only draws a green circle. The Button sample demonstrates writing a simple subclassed Windows and how to change various properties on it, and fire events in response to windows events. The Invisible sample shows how to write an Invisible at Runtime control. The Localize sample demonstrates how to support satellite DLL localization in your server. The AutoSample is a sample of how to write an OLE Automation server, which this framework supports. To create an automation server, Wizards\AutoWiz.Vbp should be run. Finally, the FontColor sample demonstrates how to get around an OLE automation problem with properties of types from imported type libraries [see Section 3, "Working with your OLE Control" for more information]. 1.3 Target Environment This framework was developed assuming you have the Microsoft Visual C++ 4.0 toolset in your path. The makectl.inc and tools.inc in the \Include directory use various tools from the toolset. In addition, the various wizards assume you have uuidgen.exe, which is included in the Microsoft Win32 SDK as well as VC4. If you are using a different environment, it should not be terribly difficult to change the variables to work with it instead. If you only have the Visual C++ 2.x tools and headers in your path, you will get various compile errors along the way. All of the makefiles and build processes are command-line based. Various people have reported, however, that it's largely trivial to integrate that into their favorite environments. NOTE: Under Windows 95, Visual C++ will not, by default, register it's environment variables to set up for command line builds. To enable command line building under Windows 95, you'll either have to chdir c:\msdev\bin and type 'vcvars32 x86' to set up your environemnt variables, or include the above file in your autoexec.bat file. Occasionally, you will get a bunch of "Out of Environment Space" messages when doing this. In the properties dialog for the Command Prompt, you can increase the size of the environment from 'Auto' to some number like 1024, and this takes care of the problem. None of this should happen when developing under Windows NT Workstation. The file 'dwinvers.h' is a file that contains version and copyright information that should be generated each time you run your builds. The framework does not do this work for you. 2.0 Creating an OLE Control =-------------------------= Creating an OLE control using this framework isn't a terribly difficult task. The provided wizards will do most of the work for you, but we will go into the process in brief here. Templating off the sample code should be able to fill in the holes. The framework implements the core functionality in a few C++ classes, notably CAutomationObject, COleControl [which inherits from CAutomationObject], and CPropertyPage. All objects inherit from CUnknownObject, which provides the support for aggregation. So, to write an OLE Control, you need to declare a new object which inherits from COleControl. In addition, you'll need to inherit from some sort of Automation interface that describes the properties and methods for your control, say, IMyControl. This interface description is generated by MKTYPLIB and will be put in some output file created by MKTYPLIB [the Wizard sets up an environment where this file is named MyControlInterfaces.H]. COleControl has a number of virtual methods that are declared as pure, which you simply must implement in your control class. These include, WindowProc, LoadBinaryState, LoadTextState, SaveBinaryState, SaveTextState, OnDraw, and RegisterClassData. To write a property page, you declare a new object which inherits from CPropertyPage. This object must implement a DialogProc. You can also implement automation objects and collections by declaring a new object that inherits from CAutomationObject. Since an OLE control is an In-Process OLE Server, you also need one file to describe all your objects, whether they be controls, automation objects, or property pages. This file will have a bunch of information in it, including a table of all objects and information about them. In addition, it'll have information on whatsort of localization your server would like to use, and what sort of licensing support you'd like to have. Finally, you'll need a resource file, an .ODL file to describe your interfaces and event interfaces, a .DEF file for your linking information, and a file to define all the guids that have been declared. While you can template all this information from one of the sample controls, using the wizard to create a new control remains the easiest way. 2.1 Using the Control Wizard The control wizard is a simple Visual Basic 4.0 program that can be used to create a new OLE Control project. It is not the most horribly robust program, and usually will just abort if something odd comes up, but it will save you a considerable amount of time in the beginning. We will now walk through a sample to create a control. We will assume that the Framework lies in C:\Controls, and we will create a subclassed scrollbar control. First, start up VB4, and run CtlWiz.Vbp. It will ask you for the name of your new control. We will use SuperScroll. We will choose to subclass a windows control, and will use satellite localization, since foreign markets are important to us, and we will not choose to avoid long file names. On the next screen, from the combo box, we will choose the SCROLLBAR window class. After that, we will choose to put the project in C:\Controls\SuperScroll. [please note that the end project generated by this wizard will only work if you choose to put it in a sub-directory of your framework root, ie C:\Controls]. Finally, we will be asked for the Location of the Template files. We will enter C:\Controls\Template. The control wizard will then generate your project, assuming that uuidgen.exe is somewhere in your PATH. This will not be the speediest of processes, since I am not a Basic Lord[tm] [on a 486/66, this takes about 30 seconds]. 2.2 Building the Control To build your control, you first need to generate the libraries for the Framework files. Doing this is as simple as going to the \Framewrk directory, and then in each of the Debug and Release Directories, typing 'make dep all'. Once the framework files have been built, go back to your control's Debug and/or Release directories, and type 'make dep all' there. This will build your control. you will only ever need to recompile the framework files if you make a change to a file in the \Include or \Framewrk directories. 2.3 Working with the In-Process server Your OLE control, any property pages, and automation objects are all just OLE COM objects in an in-process server. All of these objects must be declared in a global table, g_ObjectInfo, which is found in the main in-proc server file. Each object is declared with a wrapper; one of CONTROLOBJECT, PROPERTYPAGE, or AUTOMATIONOBJECT. The name of the object is entered as an argument to the macro. In the header file where you declare the COM object, you'll need to use one of DEFINE_PROPERTYPAGEOBJECT, DEFINE_CONTROLOBJECT, or DEFINE_AUTOMATIONOBJECT to actually declare the object for use in the global table. If it turns out that you are declaring objects that aren't creatable from a class factory, they still do need to be declared in the global table, but Creation function specified in the structure should be left as NULL. There is some additional information that must be put in the file for the in- process server. The LIBID of the type library [.TLB] must be put in the global variable g_pLibid. You must indicate what sort of localization your control supports by using the variables g_fSatelliteLocalization and g_lcidLocale. See Section 7.0 "Localization" for more information about this. In your in-proc server file, there are five routines that you can put code in. The first two are called when the DLL is first loaded and unloaded in to memory. this is a good place to do any sort of initialization that can't be put off until later. It is worth noting that for performance reasons, delaying as much as possible is often a good idea. The CheckForLicense function lets you decide if the control is licensed or not to run. The global variables g_wszLicenseKey and g_wszLicenseLocation describe the license key and the location in the registry of the license location. if you don't bother with licensing, then you can leave all of the above untouched. The RegisterData and UnregisterData routines are called from DllRegisterServer and DllUnregisterServer, and can be used to register and clean up additional information in the registry. Finally, two small pieces of code are included in this file so that your project does not have to link with any of the C-runtimes. This typically results in smaller DLL size, and can help with performance. If you want to link with the C runtime libraries, or the CRTDLL libraries, then you can remove these last things from the in-proc server file. 3.0 Working with your OLE Control =--------------------------------= Once you have your control up and running, you'll want to start extending it's functionality. The first thing to note is that your control is, in many ways, much like a regular Windows window. You have an HWND, you have a window proc, and you have to paint the client area yourself using regular windows drawing APIs and handles to Device Contexts [DC's]. 3.1 Structure of a Control There is a core set of methods that every control in this framework must implement, based on creation semantics and methods that COleControl simply does not provide for you. There are also a bunch of routines that are interesting to override and provide an implementation for. The following is a discussion of many of these. a. static Create() function. Using the control wizard, this routine is generated for you. Every control must create their control object in this routine, and then return a pointer to it's private unknown [for aggregation support]. b. Constructor and Destructor. These are also generated for you, and a control should initialize anything here. Controls should try to minimize the amount of work that is done here in order to help prevent load time from degrading unacceptably. c. RegisterClassData(). All controls must implement this routine. This routine will only be called once the first time a control of a given type is loaded in a process. Controls should register their window class [using RegisterClass and the WNDCLASS structure] here. In addition, subclassed windows controls should get a pointer to the parent control's WindowProc and set that up in the g_ObjectInfo table using the SUBCLASSWNDPROCOFCONTROL() macro. See the Button and Circle control samples for examples of how this is done. Invisible at Runtime controls should just return FALSE in this routine, as it should never get called. d. BeforeCreateWindow() and AfterCreateWindow() are not mandatory to implement, but are extremely interesting routines. BeforeCreateWindow is called right before the call to CreateWindow(), but after persistent state has been loaded. Controls should use this opportunity to set the window title for their control in m_szWindowTitle, and can also set up bits in m_dwWindowStyle, and m_dwWindowStyleEx for calls to CreateWindowEx. Doing this work here instead of the WM_CREATE case typically results in better per- formance. e. InternalQueryInterface(). Your control implements this to support the QI for your primary dispatch interface, such as IMyControl. You can also use this method to support additional interfaces in your control. For example, if you want to support IPerPropertyBrowsing, you'd have your CMyControl class inherit from IPerPropertyBrowsing, and support the QueryInterface for IID_IPerPropertyBrowsing in InternalQueryInterface. If you fail the QI, then you should delegate back to COleControl::InternalQueryInterface to see if it likes the IID. f. LoadTextState, LoadBinaryState, SaveTextState, SaveBinaryState. All controls must implement these persistence interfaces. See Section 4.0 "Persistence" for a discussion of these interfaces. g. OnDraw. This routine is called when your control is expected to draw itself. In Design mode, this call will originate from a container calling IViewObject2::Draw. In run mode, the controls framework will intercept the WM_PAINT message, and will translate it into a call to your OnDraw routine. See section 3.2 "Painting a Control" for more information on this routine. h. WindowProc. Messages that are not handled in the framework code [such as SimpleFrame messages and WM_PAINT], are sent to your control here. Your control should deal with these here. See Section 3.3 "Handling Messages in a Control" for a discussion of this routine. In addition, please see the note below on OnSpecialKey for more information. i. OnSpeicalKey. Messages for various keyboard events, such as moving the Cursor keys, function keys, and other non-standard keys do not go to the WindowProc. Instead, they are sent to the OnSpecialKey routine. Controls that want to handle special keys and accelerators should override and implement this routine. They should return TRUE if they handle a key, or FALSE if they ignored it. j. DoCustomVerb. If your control chooses to implement custom verbs in addition to the property page one that is provided by default [provided your control has a property page], then you should implement this routine, and take appropriate action depending on what verb was sent in. Return OLEOBJ_S_INVALIDVERB if you don't recognise the verb given. k. OnSetExtent. This is called every time your control is resized. The m_Size SIZEL structure is your control's current size in pixels. controls should look in here for their size information, and override OnSetExtent if they want control over how their control is sized. Please see the Invisible sample for an example of a control that is of a fixed size. In addition, the following methods/routines can be called by an OLE control, and often prove to be extremely useful. a. DoSuperClassPaint. Subclassed windows controls can call this from their OnDraw routines to paint themselves. For most windows controls, this will paint them correctly in design mode and run mode. Some Windows controls, however, will prove some- what moody, and might require a little extra tweaking. b. RecreateControlWindow. Again, used for Subclassed Controls -- will go and re- create the control's HWND. This is useful if you're changing a style bit that simply can't be changed with a SetWindowLong(GWL_STYLE ...) call. c. DesignMode. Returns a BOOL indicating it's best guess as to whether you're in design mode or not. if it can't figure it out, returns FALSE. d. GetAmbientProperty. This routine is used to get an ambient property from the container. Not all containers will return these [they might not support them], so be careful to check the return code. e. GetAmbientFont. Gets the current ambient font. Don't forget to release the font once you're done with it. Again, the host can simply not implement this. f. ModalDialog. Controls must call this before they show a modal dialog. This is seen when you're about to show your About Box dialog. g. InvalidateControl. Much like the InvalidateRect API, but this also operates in design mode. This will force your control to be repainted if you pass in NULL for the rectangle, or will just invalidate the given area if it's not NULL. h. SetControlSize. Control's who are changing their size out of OnSetExtent should use this routine to set their size. You pass in a SIZEL structure in PIXELS, and should expect a call to OnSetExtent. Be careful of some recursive situations. i. PropertyChanged. Whenever the value of a property changes, this routine should be called to notify a host. This will cause hosts to update any property browsers [such as those seen in Microsoft Visual Basic 4.0] j. RequestPropertyEdit. Whenever you want to change a property that you've marked as requestedit in the .ODL file, you need to call this first, and check the return code. k. GetResourceHandle. Controls should call this whenever they're loading a resource that could be localized. This will go and get the handle to the appropriate DLL, and deals with satellite DLLs or the lack thereof. Please see the Localize sample for how this works. l. FireEvent. You pass this routine an EVENTINFO structure, and an event as described in the EVENTINFO will be fired. You also pass paramters to this routine, as it is a varargs method. m. ControlFromUnknown. Property page code often finds it useful to get the COleControl * pointer from the IUnknown for a control object. This routine does just that. n. Exception. Your control, or any automation objects, can use this to send the user an error message. Please see Section 3.8 "Throwing an Exception" for more information on using this routine. 3.2 Painting a Control The OnDraw routine is called whenever you need to paint your OLE Control. Some- times the origin is from IViewObject2::Draw [as in design mode], and other times it comes from being sent a WM_PAINT message [as handled in ControlWindowProc]. Your control is given a DC, a rectangle to describe where to paint, a rectangle for describing a meta-file, and an Information Context [IC, passed in as an HDC] that describes the device. If the device is a metafile, then you must do a little different work. However, if the device is a raster display, you are typically painting to the screen. Your control must be careful not to make any assumptions about the DC, except that it will be in MM_TEXT mapping mode. Often, there will be no default pens, brushes, fonts or colors selected into the DC. Your control will have to do this work itself. This will typically manifest itself by having your control look slightly different in design and run modes. 3.3 Handling Messages in a Control Your control has a method called WindowProc, which is called whenever a message is sent to your control. Your control should respond to messages in the desired fashion here. Again, try to reduce the amount of work that is done in WM_CREATE, and see if it can't be put in BeforeCreateWindow. For certain types of messages, such as keyboard messages for arrow keys, and other special keys, your WindowProc routine will not get called. Instead, you'll find OnSpecialKey called instead. This code should look for WM_KEYDOWN/UP, WM_CHAR, and other messages and deal with them as appropriate. There is a certain class of messages that typically involve notify a window about happenenings that are usually sent to a window's parent. These include WM_COMMAND WM_NOTIFY, WM_CTLCOLOR, etc. These messages will be reflected by the host to your control in the form of OCM_COMMAND, OCM_NOTIFY, OCM_CTLCOLOR, etc. Your controls should look for these messages instead of WM_COMMAND, etc. Please see olectl.h for other OCM_ messages that you might be interested in. 3.4 Adding a Property One of the more important parts of a control is often the set of properties. When you create a control with the wizard, you have no properties by default. To add them is a relatively straightforward and simple process. First, you need to modify the primary dispatch interface for your control in the .ODL file. For example, lets's say i've got a control called SuperScroll, and i'd like to add a LargeChange method to the control. I'd add the following to the ISuperScroll inter- face description in the .ODL: [id(DISPID_LARGECHANGE), propget, helpstring("The largechange property")] HRESULT LargeChange([out, retval] long *plLargeChange); [id(DISPID_LARGECHANGE), propput] HRESULT LargeChange([in] long lLargeChange); DISPID_LARGECHANGE is something that i define in dispids.h. I then regenerate the type library [.TLB] file by typing: make SuperScroll.TLB More importantly, this regenerates SuperScrollInterfaces.H. I can then cut and paste the following two lines from said header: STDMETHOD(get_LargeChange)(THIS_ long FAR* plLargeChange) PURE; STDMETHOD(put_LargeChange)(THIS_ long lLargeChange) PURE; I take these two lines and add them to my class description for CSuperScroll, and make sure that I remove the PURE declarators at the end; ie: STDMETHOD(get_LargeChange)(long FAR* plLargeChange); STDMETHOD(put_LargeChange)(long lLargeChange); I can now implement these methods in my control file to implement my property. Please note that there are a few standard DISPIDs defined for you in olectl.h. Whenever you want to declare a property, take a look in this header first to see if there is a standard dispid for it first. 3.5 Adding a Method Adding a method is much like adding a property to your control. First thing one does is define a DISPID for the method. Once you've got that, it's as simple as adding the method to the primary interface for you control. Controls generated with the Control wizard will already have an About method defined for them. Lets say we'd like to define a method called MooCow, with three parameters, the last of which is optional. Here is one such method: [id(DISPID_MOOCOW), helpstring("Makes your Cow moo")] HRESULT MooCow([in] long lSeconds, [in] boolean fLowPitch, [in, optional] VARIANT vPitch); Again, as with properties, you regenerate the type library, and then paste the declaration in to your control header [without the PURE declarator] and implement it. 3.6 Adding an Event In a great many situations, one will want to fire an event. For example, when one gets a WM_?BUTTONDOWN message, it often makes sense to fire a MouseDown event. This turns out to be a surprisingly easy thing to do. The first thing that has to be done is the event has to be defined in an EVENTINFO structure. There are many ways to do this, including declaring a new global variable for each event type, or using an array. We will talk about the latter, since it's a little neater. Let's say i want to have KeyDown, KeyUp, and KeyPushed events. Here's how i might declare them: typedef enum { MyCtlEvent_KeyDown = 0, MyCtlEvent_KeyUp = 1, MyCtlEvent_KeyPushed = 2 } MYCTLEVENTS; VARTYPE rgI2 [] = { VT_I2 }; EVENTINFO m_rgMyCtlEvents [] = { { DISPID_KEYDOWN, 1, rgI2 }, { DISPID_KEYUP, 1, rgI2 }, { DISPID_KEYPUSHED, 1, rgI2 } }; The EVENTINFO structure has three members; the dispid of the event, the count of arguments in the event, and a pointer to an array of VARTYPEs that describe the types of the parameters to the event. Please note, again, that there are a bunch of DISPIDs defined for you in olectl.h. Whenever you're adding an event, go check there first to see if there's already a DISPID for it. To fire these events from code, i can just call the following: FireEvent(&(m_rgMyCtlEvents[MyCtlEvent_KeyDown]), sKeyValue); 3.7 Using Standard OLE Types Many controls will find it useful to declare properties of types provided by OLE, such as Font, Picture, and Color. Many hosts will detect properties of these types and put up convenient browsers for the user to select values for these types. To declare a property of one of these types, one must first make sure their .ODL includes the following at the top: importlib(STDTYPE_TLB); Then, to declare a property of type Font, Picture, or Color, one would do some- thing similar to the following, depending on the type: [id(DISPID_FONT), propget] HRESULT Font([out, retval] IFontDisp **ppFont); [id(DISPID_FONT), propput] HRESULT Font([in] IFontDisp *pFont); [id(DISPID_MOUSEICON), propget] HRESULT MouseIcon([out, retval] IPictureDisp **ppMouseIcon); [id(DISPID_MOUSEICON), propput] HRESULT MouseIcon([in] IPictureDisp *pMouseIcon); [id(DISPID_FORECOLOR), propget] HRESULT ForeColor([out, retval] OLE_COLOR *pocForeColor); [id(DISPID_FORECOLOR), propput] HRESULT ForeColor([in] OLE_COLOR ocForeColor); For the get_ and put_ methods for these types, you'll get a property as declared above. For font's and pictures, you'll probably want to QI these for IFont and IPicture respectively. See the FontColor control sample for an idea of how this is done. MSDN contains some very good descriptions of how to use these fonts in your application, but the following is a short run-down. To use a font object in your control, you'll typically call the get_hFont method, and pass the resulting HFONT to your DC. Pictures will be much the same. To use a color, you'll want to call OleTranslateColor to convert it to a real COLORREF. OLE_COLORs are basically COLORREFs with some support for 'generic' colors, such as COLOR_WINDOW, COLOR_WINDOWTEXT, etc. To convert one of these into an OLE_COLOR, just OR [|] them with 0x80000000. Ie, to initialize your background to COLOR_WINDOW, set your backcolor property to COLOR_WINDOW | 0x80000000. Then, to paint your backdrop, just call OleTranslateColor(), and use the resulting colorref. NOTE: The Framework uses dual/vtable bound automation interfaces, and uses OLE Automation functionality to support IDispatch methods on the automation objects. There is a known problem in OLE automation, which will cause problems [unexpected failures and/or crashes] when using the provided ITypeInfo::Invoke on properties that are declared to be of types that are imported from a type library [ie, any font, picture, or color property has this problem.] The way to get around this problem is to override Invoke(), and to look for the DISPIDs of your properties that are of this type. In this case, you can quickly dispatch the call to the appropriate member function yourself. The FontColor sample does just this. See it's implementation of IDispatch::Invoke for sample code on how to work around it. This problem will not exist in a future version of OLE Automation, but until then, the workaround is necessary -- but fortunately not terribly expensive. 3.7 Throwing an Exception Every once in a while, during an operation, your control will find itself rather upset with the state of the union, and will wish to communicate this to the user. The way to do this is via an exception. In any of your OLE Automation methods or property operators, you can call the Exception method when exiting, and it will set up all the appropriate information to trigger the error. For Example: CMyControl::put_ButtZilla(long lButtZilla) { if (ButtZilla == 10) return Exception(MYCTL_E_IHATETHENUMBER10, IDS_ERR_IHATETHENUMBER10, 0); m_lButtZilla = lButtZilla; return S_OK; } The arguments to the Exception routine are as the follows. The first is the SCODE of the error you wish to trigger. For errors unique to your control, you should define them something like: #define MYCTL_E_IHATETHENUMBER10 MAKE_SCODE(SEVERITY_ERROR, FACILITY_CONTROL, 34500) The second argument is the resource id of the string that you should display. The Exception code will correctly get this information from your localized Satellite DLL. Finally, the last argument is the helpcontextid that will passed to the helpfile you defined in your object's structure. 4.0 Persistence =-------------= One of the most important things your control will do is save out and restore it's persistent state. This will typically be done in one of two ways; through PropertyBags for text persistence, and through Streams for binary persistence. In the latter, performance is absolutely critical to make your control load quickly. The OLE Controls Framework requires that your control implement four member func- tions to support persistence. You are required to implement LoadTextState, LoadBinaryState, SaveTextState, and SaveBinaryState. If you are positive that you are never going to be in a host that uses IPersistPropertyBag, then you can ignore the two text interfaces, but this isn't recommended [they prove sufficiently straightforward to use]. 4.1 Text Persistence Text persistence in the Framework is done via IPersistPropertyBag and IPropertyBag. All OLE controls have an implementation of IPersistProperty Bag, and are given pointers to PropertyBag objects to do their work. MSDN and Craig Brockschmidt's "Inside OLE 2 [2nd ed]" both have descriptions of the IPropertyBag interface. Effectively, there are two routines that the programmer will use: Read and Write. In both cases, the programmer will pass in a VARIANT. in the Read case, the property, if it was saved out, will be put in the VARIANT. If the property couldn't be found [it wasn't ever saved out], then the default value for that property should be used, and an error should probably not be returned. For the Write, a VARIANT with the data is passed in. To persist out a collection or an object [such as a Font or Picture object], one can pass in a VT_UNKNOWN object, and the PropertyBag will then QI that object for IPersistPropertyBag or IPersistStream and persist it. This actually proves effective for persisting collections -- they can just support IPersistPropertyBag. All of the samples except the Localize and Circle sample have examples of how to persist out properties using PropertyBags. For controls with many properties, it often makes sense to head to some sort of table driven persistence to reduce code size and bug potential. 4.2 Binary Persistence Binary persistence turns out to be the more critical things to work on when implementing an OLE control. Control Load speed can be severely hampered by a poorly written LoadBinaryState routine, so it's rather critical to spend some time thinking about how to keep this routine fast. The binary persistence code is used by many hosts all the time, and by other hosts when including the control in a generated executable file. In both routines, you are handed a pointer to an IStream object. The key to load speed here is to reduce the number of operations on the stream. For save, this is slightly less critical. Typically, a control will want to save out the following information [often in the given order]: - some sort of header with a magic number, version, and size information - fixed size state information, such as longs, floats, colors, strings, etc. - variable sized persisten state, such as fonts, pictures, collections, etc. Most control writers will want to start out their binary persistent state with some sort of header structure that includes a 'magic' number that you can check for when you're loading for sanity. You'll also want to include some sort of version number so that future versions of your control can deal with older versions, and finally one will often want to write out the number of bytes of data that were written. The samples in the framework that have a binary persistent state use the following structure: #define STREAMHDR_MAGIC 0x12345678 typedef struct { DWORD dwMagic; DWORD dwVersion; DWORD cbSize; } STREAMHDR; The SaveBinaryState routine saves out this information, and the LoadBinaryState routine looks for it. One way to write out all the fixed size information [and therefore load it efficiently] is to do it all in one chunk -- if all your fixed persistent state is in a structure in your control object, then you can just write out said structure in the persistence code. Controls generated by the control wizard will have a structure defined for them called MYCTLNAMECTLSTATE which users can opt to put their fixed state data into. Then, when saving, they can just do the following: hr = pStream->Write(&m_state, sizeof(m_state), NULL); For loading, it becomes as simple as: hr = pStream->Read(&(m_state), sizeof(m_state), NULL); Extremely efficient and simple. For fonts and pictures, it's slightly more complicated. Effectively, one has to QI those objects for IPersistStream, and then call the Load or Save routine with the stream that you've been given. Typically, this can be done after you've written out all other information. The FontColor control sample does just this. If you follow the above suggestions for persistent state structure, you can typically have your load routine look something like as follows: IPersistStream *pps; STREAMHDR sh; HRESULT hr; // first read in the streamhdr, and make sure we like what we're getting // hr = pStream->Read(&sh, sizeof(sh), NULL); RETURN_ON_FAILURE(hr); // sanity check // if (sh.dwMagic != STREAMHDR_MAGIC || sh.cbSize != sizeof(m_state)) return E_UNEXPECTED; // read in the control state information // hr = pStream->Read(&(m_state), sizeof(m_state), NULL); RETURN_ON_FAILURE(hr); // now read in the font! // OleCreateFontIndirect(&_fdDefault, IID_IFont, (void **)&m_pFont); RETURN_ON_NULLALLOC(m_pFont); // qi it for ipersiststream and load it in. // hr = m_pFont->QueryInterface(IID_IPersistStream, (void **)&pps); RETURN_ON_FAILURE(hr); hr = pps->Load(pStream); pps->Release(); return hr; This proves to be acceptably fast and robust. 5.0 Property Pages =----------------= Most OLE Controls will find property pages an invaluble addition to their design time functionality. Fortunately, implementing them proves to be relatively straight- forward. To do so, one merely needs to declare an object that inherits from CPropertyPage. 5.1 Working with a Property Page Your property page is declared in the header file using the DEFINE_PROPERTYPAGE macro, which puts it into the g_ObjectInfo table. The framework supports the creation of the property page object, but you are required to implement the static Create() function [which the control wizard generates for you]. The property page is created much like a regular windows dialog box would be -- you use your favorite resource editor to create a DIALOG resource, and then just cut and paste it into your control's resource [.RC] file. The most important method you'll have to implement will be the DialogProc method, which is where all the work will take place. In addition to the regular windows messages that one would expect in a DialogProc, there are three additional ones which people working with this framework will expect: a. PPM_NEWOBJECTS -- your control has been given some new objects. You are expected to go and populate your page's controls with information from this object. Using the FirstControl() and NextControl() methods from the CPropertyPage class, you can get the relevant information. b. PPM_APPLY -- you have to apply any changes that have occurred now. Again, you can use the FirstControl() and NextControl() routines to loop through all the objects for which the property pages were visible and apply the values [note that it's possible for there to be more than one object for which a property page is being displayed]. c. PPM_EDITPROPERTY -- when you are sent this message, you are expected to set the focus to the control which represents the property of the given DISPID. You will typically only see this message called if you implement IPerPropertyBrowsing and return a value in MapPropertyToPage. Please see one of the sample controls for exact details on these messages. 5.2 Navigating through Associated Objects Your property pages will operate on one or more controls. When initializing, one will typically get some values from the first control that you are given. you can use the FirstControl() method to get the object pointer for this control. You can then QI it for you primary dispatch interface to get properties to populate the page with. When told to apply the values [PPM_APPLY], you'll want to apply them to all objects, which means you'll want to loop using FirstControl and NextControl(), as follows: for (pUnk = FirstControl(&dwCookie) ; pUnk; pUnk = NextControl(&dwCookie)) { hr = pUnk->QueryInterface(IID_IButton, (void **)&pButton); if (FAILED(hr)) continue; GetDlgItemText(hwnd, IDC_CAPTION, szTmp, 128); bstr = BSTRFROMANSI(szTmp); ASSERT(bstr, "Maggots!"); pButton->put_Caption(bstr); SysFreeString(bstr); pButton->Release(); } Please note that the return values of FirstControl and NextControl don't need to be Release()'d. 5.3 Marking your Page as Dirty. It is moderately important to correctly mark your property page as dirty at the appropriate times. This must be done manually. Typically, one will do this in response to a windows notification message, such as EN_CHANGE or BN_CLICKED. When you wish to mark your page as dirty, the MakeDirty() routine should be called. This will cause the Apply button to be enabled, if it was previously disabled, and will tell the host that you should be saved before destroying the page. The following code causes the page to mark itself as dirty when the user changes the text in a Text box in the property page: case WM_COMMAND: switch (LOWORD(wParam)) { case IDC_CAPTION: if (HIWORD(wParam) == EN_CHANGE) MakeDirty(); break; } break; 6.0 String Manipulation =----------------------= The OLE Controls framework provides a robust system of macros for manipulating strings in pretty much all of the ways that you'll run into while working with an OLE control. 6.1 Types of Strings Under 32bits, there are a few different types of strings, and understanding these tends to be pretty important when working with OLE, since there is great potential for memory leaks and bugs associated with strings. There are two fundamental types of strings -- Multi-Byte [which can be ANSI or double byte] and Unicode strings. Of the former, one almost always works with some sort of char * pointer [LPSTR, LPCSTR]. Of the latter, there are a few types that are commonly used. Most notably, there are WCHAR * [LPWSTR, LPWCSTR], BSTR, and OLESTR strings. LPWSTR pointers are just that -- a pointer to a wide string. An LPOLESTR pointer is much the same, with some additional OLE rules added to it. An OLESTR is merely a wide string, but when it's an out-parameter to a function, it should be allocated using the host's IMalloc allocator [ie, CoTaskMemAlloc]. A BSTR is a string with a little more structure [specifically, a length prefix]. To work with BSTRs, you need to use special APIs designed exclusively form them, notably SysAllocString, SysFreeString, and SysStringLen [there are a few others. See the OLE Programmers Reference, Volume II for more details]. These data types are fully interchangeable as far as compares and copies go, but they are NOT interchangeable as far as allocation and freeing go -- it is not acceptable to call SysFreeString on an OLESTR or LPWSTR string. Both BSTRs and OLESTRs as in-parameters to functions should not be freed [as per standard OLE COM conventions]. By the same token, BSTRs and OLESTRs as out params should be expected to be freed, and should thus be allocated appropriately. 6.2 Working with Strings Now, the problem is that, for the most part, your controls will be be working with Multi-byte strings, excepting for where you work with OLE. Therefore, there will be various scenarios where you'll either be given a wide string, and need the multi-byte version of it, or you'll have a multi-byte string, and need a wide string for it. To solve these problems, the OLE Controls Framework includes the following macros to work with: MAKE_WIDEPTR_FROMANSI(newstringname, convertme) MAKE_ANSIPTR_FROMWIDE(newstringname, convertme) BSTRFROMANSI(ansistr) OLESTRFROMANSI(ansistr) BSTRFROMRESID(resourceid) OLESTRFROMRESID(resourceid) COPYOLESTR(copyme) COPYBSTR(copyme) The first two macros will take a string of a given type and a name, and create a variable of the new name [do -not- declare a variable of this name yourself], and then convert the other string into the new variable. This cannot be used as an rvalue in C/C++ expressions, nor can it be an lvalue [it pretty much needs to sit on a line by itself]. The last set of macros pretty much do all the remaining interesting work. You can get BSTRs or IMalloc'd OLESTRs from an ANSI string, or copy OLESTRs and BSTRs. The only additional functions of real interest are those that take a WORD, which is a resource id, and loads in a string from your localization DLL [or the main if you don't do satellite localization] and makes either a BSTR or OLESTR out of it. This proves useful in a few places where you need a localized string. Remember that while these macros were designed with a certain amount of speed in mind, converting strings is still not a ridiculously cheap operation, and control writers should try to be moderately conservative in string conversions. 7.0 Localization =---------------= The OLE Controls framework also has some support for robust localization of your control, most notably, property pages, type libraries, and anything else you find interesting to localize. The scheme is follows: the resources for the default language [typically english] are in the main in-proc server's resources. Then, for each additional language supported, there is a satellite DLL which contains the type library and resources for that language. In the main in-proc server you define a table in the resources which has all of the supported languages and their localized DLLs names. The table will look as follows: INTLSZ_LANGMAP RCDATA DISCARDABLE BEGIN // Primary Language ID SubLanguage ID Satalite DLL // ------------------- -------------- ------------ LANG_FRENCH, SUBLANG_SWISS, "LocalizeFS.DLL", "\0", LANG_FRENCH, SUBLANG_FRENCH, "LocalizeFR.DLL","\0", LANG_JAPANESE, SUBLANG_DEFAULT, "LocalizeJP.DLL","\0", END The search rules are as follows: If you find an exact match of both the primary langid and sublandid for the current ambient LCID in the table, then return the DLL name for that entry. If only a primary langid match is found, then return the last matching entry with that primary langid from the table. [thus FRENCH/FRENCH comes after FRENCH/SWISS]. If there are no matches, the currently running DLL is used. Thus, if running under a FRENCH/SWISS system, you would use LocalizeFS.DLL. If running under a FRENCH/CANADIAN system, you'd ust LocalizeFR.DLL, and if you were running under a SPANISH/MEXICAN system, you'd use Localize.Ocx for resources. 7.1 Setting up for Localization If you want to support satellite localization in your in-proc server, then you need to set up a couple of things. First, you'll need to set the variable g_fSatelliteLocalization in your in-proc server file to TRUE. This will instruct all further code paths to use satellite local- ization. You'll also need to set up the above table in your .RC file for the main in-process server. Add entries for the langauges you wish to support. Whenever you want to load a resource, make sure you use GetResourceHandle() to get the instance handle for the localized resources. This will traverse the table and find the correct satellite DLL and load it. Please see the Localize sample included with the framework for an example of how this works, along with an actual localized satellite DLL [in the \French subdirectory]. The satellite DLL should contain all localized resources you're interested in as well as a localized type library. Don't forget to mark the localized type library with the LCID of the intended language. 8.0 Internet Controls =-------------------= Making your control Internet aware is simple. What follows is a description of the steps required. 1. Make sure your control inherits from CInternetControl rather than COleControl. 2. Call the SetupDownload method with either a URL, Moniker, or a PropertyStream to initiate the download of the data. 3. Implement the OnData method to receive the stream information. The control should understand how to process the data as it arrives to improve the user experience. In URLDib2, the control distinguishes between bitmap header data and actual bits, and begins to create the DC etc. while it is waiting for more bits to come down. 4. Implement the OnProgress method to be called while data is being downloaded. A control can determine what percentage of the data has been transferred, to determine how long to wait, display a progress gauge etc. Additional Support The following methods are also available: GetAMoniker - turn a URL into a Moniker GetAsyncHost - In case you want to know the host GetBinding - converts a propId to an IBinding InternalQueryInterface - allows you to expose your own interfaces. 9.0 Miscellaneous =---------------= 9.1 Recommended Reading Inside OLE2, Second Edition, Kraig Brockschmidt, Microsoft Press. - Chapters 3, 13-15 and 24 contain most of the information you'll need to work with an OLE control and/or automation server. The chapters on persistence should also prove useful for those unfamiliar with them. OLE2 Programmers Reference, Volume I and II, Microsoft press. MFC Source Code, Visual C++ 4.0. - when looking for ideas on how to do something, the CDK source code proves to be an invaluble resource. Use it -- frequently. 9.2 Host Specific Notes Then following are notes, things to consider, or known problems with specific OLE Controls hosts 9.2.1 Microsoft Access 95 Access 95 -requires- IPerPropertyBrowsing. If you do not implement this routine, you will not able to put one of your controls on a form. See MSDN and the MFC source code for documentation on this method. In addition, you cannot return error scodes from GetPredefinedStrings. You must return either S_OK or S_FALSE. Returning an error scode will crash. MapPropertyToPage should still return PERPROP_E_NOPAGEAVAILABLE if there are no pages available.